﻿/*************************************************************************
 * Created by OhZee Creative, LLC
 * 2013
 * This code is licensed under Microsoft Public License (Ms-PL)
 * http://opensource.org/licenses/ms-pl
 * http://ozcontrols.codeplex.com/
 *************************************************************************/

namespace OhZeeCreative.UI.Controls
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using OhZeeCreative.Helpers;
#if SILVERLIGHT
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Shapes;
    using System.Windows.Media;
#endif
#if NETFX_CORE
    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Shapes;
    using Windows.UI.Xaml.Media;
#endif
    /// <summary>
    /// Starburst control draws a specified number of converging rays emanating from a given point. Similar to Sunburst, you can offset the origin and apex separately. The AngleSweep property allows to create starbursts whose arc spans less than a full circle. The InnerRadiusRatio controls how spiky the rays are. This is a very flexible control that you can use to create a regular (5-point) star.
    /// </summary>
    [EditorBrowsableAttribute(EditorBrowsableState.Always)]
    [TemplatePart(Name = Starburst.TEMPLATE_PART_rays, Type = typeof(PathFigure))]
	public sealed class Starburst : Control
	{
        /// <summary>
        /// The name of the PathFigure element in the ControlTemplate used to draw the rays.
        /// </summary>
        public const string TEMPLATE_PART_rays = "rays";

		private PathFigure pathFigure;

		// Shared properties with Sunburst
		public static readonly DependencyProperty OriginXProperty = DependencyProperty.Register("OriginX", typeof(double), typeof(Starburst), new PropertyMetadata(0.5, GenericPropertyChangedHandler));
		public static readonly DependencyProperty OriginYProperty = DependencyProperty.Register("OriginY", typeof(double), typeof(Starburst), new PropertyMetadata(0.5, GenericPropertyChangedHandler));
		public static readonly DependencyProperty ApexDeltaXProperty = DependencyProperty.Register("ApexDeltaX", typeof(double), typeof(Starburst), new PropertyMetadata(0.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty ApexDeltaYProperty = DependencyProperty.Register("ApexDeltaY", typeof(double), typeof(Starburst), new PropertyMetadata(0.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty ApexShiftAngleProperty = DependencyProperty.Register("ApexShiftAngle", typeof(double), typeof(Starburst), new PropertyMetadata(0.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty ApexShiftAmountProperty = DependencyProperty.Register("ApexShiftAmount", typeof(double), typeof(Starburst), new PropertyMetadata(0.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty RaysProperty = DependencyProperty.Register("Rays", typeof(int), typeof(Starburst), new PropertyMetadata(30, RaysPropertyChangedHandler));
		public static readonly DependencyProperty RotationProperty = DependencyProperty.Register("Rotation", typeof(double), typeof(Starburst), new PropertyMetadata(0.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty FillContainerProperty = DependencyProperty.Register("FillContainer", typeof(bool), typeof(Starburst), new PropertyMetadata(false, GenericPropertyChangedHandler));
		public static readonly DependencyProperty ApexAdjustmentModeProperty = DependencyProperty.Register("ApexAdjustmentMode", typeof(ApexAdjustmentModes), typeof(Starburst), new PropertyMetadata(ApexAdjustmentModes.AbsoluteViaDeltas, GenericPropertyChangedHandler));
		public static readonly DependencyProperty PathClipProperty = DependencyProperty.Register("PathClip", typeof(RectangleGeometry), typeof(Starburst), new PropertyMetadata(null));
		public static readonly DependencyProperty EdgeProperty = DependencyProperty.Register("Edge", typeof(Brush), typeof(Starburst), new PropertyMetadata(null));
		public static readonly DependencyProperty EdgeThicknessProperty = DependencyProperty.Register("EdgeThickness", typeof(double), typeof(Starburst), new PropertyMetadata(null));

		// Unique properties
		public static readonly DependencyProperty AngleSweepProperty = DependencyProperty.Register("AngleSweep", typeof(double), typeof(Starburst), new PropertyMetadata(360.0, GenericPropertyChangedHandler));
		public static readonly DependencyProperty InnerRadiusRatioProperty = DependencyProperty.Register("InnerRadiusRatio", typeof(double), typeof(Starburst), new PropertyMetadata(0.8, GenericPropertyChangedHandler));
					

		private static void GenericPropertyChangedHandler(DependencyObject o, DependencyPropertyChangedEventArgs e)
		{
			Starburst radialShine = (Starburst)o;
			radialShine.Update();
		}

		private static void RaysPropertyChangedHandler(DependencyObject o, DependencyPropertyChangedEventArgs e)
		{
			Starburst radialShine = (Starburst)o;

			if (!DependencyHelper.CoerceMinimum(radialShine, e, RaysProperty, 2))
			{
				radialShine.Update();
			}
		}

        /// <summary>
        /// Gets or sets the origin on the X-axis. 0 is left-most edge, 1 is right-most edge.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double OriginX
		{
			get { return (double)this.GetValue(OriginXProperty); }
			set { this.SetValue(OriginXProperty, value); }
		}

        /// <summary>
        /// Gets or sets the origin on the Y-axis. 0 is top-most edge, 1 is bottom-most edge.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double OriginY
		{
			get { return (double)this.GetValue(OriginYProperty); }
			set { this.SetValue(OriginYProperty, value); }
		}

        /// <summary>
        /// Gets or sets the amount the apex will be offset from the origin in pixels on the X-axis.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double ApexDeltaX
		{
			private get { return (double)this.GetValue(ApexDeltaXProperty); }
			set { this.SetValue(ApexDeltaXProperty, value); }
		}

        /// <summary>
        /// Gets or sets the amount the apex will be offset from the origin in pixels on the Y-axis.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double ApexDeltaY
		{
			private get { return (double)this.GetValue(ApexDeltaYProperty); }
			set { this.SetValue(ApexDeltaYProperty, value); }
		}

        /// <summary>
        /// Gets or sets the angle in degrees between the origin and the apex according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double ApexShiftAngle
		{
			get { return (double)this.GetValue(ApexShiftAngleProperty); }
			set { this.SetValue(ApexShiftAngleProperty, value); }
		}

        /// <summary>
        /// Gets or sets the amount the apex will be shifted from the origin by the specified amount. A shift of 1 moves the apex to the circumference of the circle.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double ApexShiftAmount
		{
			get { return (double)this.GetValue(ApexShiftAmountProperty); }
			set { this.SetValue(ApexShiftAmountProperty, value); }
		}

        /// <summary>
        /// Gets or sets the number of rays. Must be a minimum of 1.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public int Rays
		{
			get { return (int)this.GetValue(RaysProperty); }
			set { this.SetValue(RaysProperty, value); }
		}

        /// <summary>
        /// Gets or sets the rotation of the rays. At 0 rotation, the first ray is aligned with the X-axis.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double Rotation
		{
			get { return (double)this.GetValue(RotationProperty); }
			set { this.SetValue(RotationProperty, value); }
		}

        /// <summary>
        /// Determines whether the circle should fill its bounds.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public bool FillContainer
		{
			get { return (bool)this.GetValue(FillContainerProperty); }
			set { this.SetValue(FillContainerProperty, value); }
		}

        /// <summary>
        /// Determines how to adjust the focal point of the rays. Depending on the mode, drawing is affected either by DeltaX and DeltaY properties or ShiftAngle and ShiftAmount.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public ApexAdjustmentModes ApexAdjustmentMode
		{
			get { return (ApexAdjustmentModes)this.GetValue(ApexAdjustmentModeProperty); }
			set { this.SetValue(ApexAdjustmentModeProperty, value); }
		}

        /// <summary>
        /// Prevents the shapes to exceed the bounds of the control. It's used in the control template
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public RectangleGeometry PathClip
        {
            get { return (RectangleGeometry)this.GetValue(PathClipProperty); }
            set { this.SetValue(PathClipProperty, value); }
        }

        /// <summary>
        /// Gets or sets the brush for the edges of each ray.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public Brush Edge
		{
			get { return (Brush)this.GetValue(EdgeProperty); }
			set { this.SetValue(EdgeProperty, value); }
		}

        /// <summary>
        /// Gets or sets the thickness of the edges.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double EdgeThickness
		{
			get { return (double)this.GetValue(EdgeThicknessProperty); }
			set { this.SetValue(EdgeThicknessProperty, value); }
		}

        /// <summary>
        /// Gets or sets the sweep angle in degrees starting from 0 according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double AngleSweep
        {
            get { return (double)this.GetValue(AngleSweepProperty); }
            set { this.SetValue(AngleSweepProperty, value); }
        }

        /// <summary>
        /// Gets or sets the size of the inner radius relative to the outer radius which is determined by the size of the control. For a value of 0.5 inner radius is half the length of the outer radius.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double InnerRadiusRatio
        {
            get { return (double)this.GetValue(InnerRadiusRatioProperty); }
            set { this.SetValue(InnerRadiusRatioProperty, value); }
        }


		public Starburst()
		{
			this.DefaultStyleKey = typeof(Starburst);

			this.SizeChanged += (s, e) => {
#if SILVERLIGHT
                Point point = new Point();
#endif
#if NETFX_CORE
			    Point point;
#endif
                this.PathClip = new RectangleGeometry() { Rect = new Rect(point, e.NewSize) };
				this.Update(); 
			};
		}

#if SILVERLIGHT
        public override void OnApplyTemplate()
#endif
#if NETFX_CORE
    	protected override void OnApplyTemplate()
#endif		
		{
            this.pathFigure = this.GetTemplateChild(Starburst.TEMPLATE_PART_rays) as PathFigure;
            this.Update();
		}

		private void Update()
		{
			if (this.pathFigure == null || this.Rays < 2)
			{
				return;
			}

			this.pathFigure.Segments.Clear();

			double width = this.ActualWidth;
			double height = this.ActualHeight;

			if (width == 0.0 || height == 0.0)
			{
				return;
			}			

			double offsetX = MathHelper.ActualValue(0.0, this.ActualWidth, this.OriginX);
			double offsetY = MathHelper.ActualValue(0.0, this.ActualHeight, this.OriginY);

#if SILVERLIGHT
            Point originOffset = new Point();
#endif
#if NETFX_CORE
			Point originOffset;
#endif
			originOffset.X = offsetX;
			originOffset.Y = offsetY;
			
			double[] distances = RadialHelper.GetDistances(originOffset, new Point[] { new Point(0.0, offsetY), new Point(offsetX, 0.0), new Point(width, offsetY), new Point(offsetX, height) });
			double radius = this.FillContainer ? distances.Max() : distances.Min();	
			
			if (radius <= 0.0)
			{
				return;
			}

			double innerRadius = Math.Max(0.0, radius * this.InnerRadiusRatio);

			Point focalPoint = originOffset;
			if (this.ApexAdjustmentMode == ApexAdjustmentModes.AbsoluteViaDeltas)
			{
				focalPoint.X += this.ApexDeltaX;
				focalPoint.Y += this.ApexDeltaY;
			}
			else if (this.ApexShiftAmount != 0)
			{
				double normalizedShift =  (radius - innerRadius) * this.ApexShiftAmount;
				focalPoint = RadialHelper.GetRadialCoordinate(MathHelper.Radians(this.ApexShiftAngle), normalizedShift).OffsetPoint(originOffset);
			}
			
			IEnumerable<Point> innerPoints = RadialHelper.GetRadialCoordinatesForDegrees(
				0 + this.Rotation,
				this.AngleSweep + this.Rotation,
				innerRadius,
				innerRadius,
				this.Rays + 1,
				easing: null,
				equalDeltasForAll: false)
				.Select(point => point.OffsetPoint(focalPoint));

			double angleDelta = this.AngleSweep.Halved() / this.Rays;
			
			IEnumerable<Point> outerPoints = RadialHelper.GetRadialCoordinatesForDegrees(
				this.Rotation + angleDelta,
				this.AngleSweep + this.Rotation + angleDelta, 
				radius, 
				radius, 
				this.Rays,
				easing: null,
				equalDeltasForAll: true)
				.Select(point => point.OffsetPoint(originOffset));
			
			IEnumerator<Point> inner = innerPoints.GetEnumerator();			
			IEnumerator<Point> outer = outerPoints.GetEnumerator();

			inner.MoveNext();
			this.pathFigure.StartPoint = inner.Current;

			for (int i = 0; i < this.Rays; i++)
			{
				outer.MoveNext();
				this.pathFigure.Segments.Add(new LineSegment() { Point = outer.Current });

				inner.MoveNext();
				this.pathFigure.Segments.Add(new LineSegment() { Point = inner.Current });
			}

#if SILVERLIGHT
            Size size = new Size();
#endif
#if NETFX_CORE
			Size size;
#endif
			size.Width = innerRadius;
			size.Height = innerRadius;

			SweepDirection sweepDirection = this.AngleSweep >= 0 ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;

			double deltaAngle = Math.Abs(this.AngleSweep);

			if (deltaAngle < 360.0)
			{
				bool isLargeArc = deltaAngle > 180.0;			
				this.pathFigure.Segments.Add(new ArcSegment() { Point = this.pathFigure.StartPoint, Size = size, SweepDirection = sweepDirection, IsLargeArc = isLargeArc });
			}
		}
	}
}
